/**
 * Copyright (c) 2015-2018, Michael Yang 杨福海 (fuhai999@gmail.com).
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.jfast.framework.util;


import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLDecoder;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import com.jfast.framework.kit.PathKit;
import com.jfast.framework.web.core.JfastConstant;


/**
 * 类扫描器
 * @author zengjintao
 * @version 1.0
 * @createTime 2018年1月10日下午8:14:50
 */
public class ClassScanner {

    private static final Set<Class<?>> appClasses = new HashSet<>();

    private static Set<String> excludeJars = null;
    
    public static void addJars(Set<String> jars){
    	excludeJars = jars;
    }
    
    public static List<Class<?>> getClassByAnnoation(Class<? extends Annotation> annotationClass){
    	if (appClasses.isEmpty()) {
            initAppClasses();
        }
    	List<Class<?>> classes = new ArrayList<>();
    	for (Class<?> clazz : appClasses) {
    		Annotation annotation = clazz.getAnnotation(annotationClass);
    		if(annotation == null){
    			continue;
    		}
    		classes.add(clazz);
		}
		return classes;
    }
	
	public static List<Class<?>> getClassByAnnoation(@SuppressWarnings("unchecked") Class<? extends Annotation>... annotationClass){
		if (appClasses.isEmpty()) {
            initAppClasses();
        }
		List<Class<?>> classes = new ArrayList<>();
		for (Class<? extends Annotation> annotation : annotationClass) {
			classes.addAll(getClassByAnnoation(annotation));
		}
		return classes;
	}
	
	public static List<Class<?>> getClassByAnnoation(Class<? extends Annotation> annotationClass,String basePackage){
		if (appClasses.isEmpty()) {
            initAppClasses();
        }
		List<Class<?>> classes = new ArrayList<>();
		for (Class<?> clazz : getClassByAnnoation(annotationClass)) {
			String packageName = clazz.getPackage().getName();
			if(packageName.equals(basePackage)){
				classes.add(clazz);
			}
			continue;
		}
		return classes;
	}
	
	@SuppressWarnings("unchecked")
	public static <T> List<Class<? extends T>> getSubClassByParent(Class<? extends T> parentClass){
		if(parentClass == null)
			throw new NullPointerException("parentClass can not be null");
		
		if(appClasses.isEmpty())
			 initAppClasses();
		List<Class<? extends T>> classes = new ArrayList<>();
		for (Class<?> clazz : appClasses) {
			if(parentClass.isAssignableFrom(clazz) && !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()))
				classes.add((Class<? extends T>) clazz);
		}
		return classes;
	}
    
    /**
     * 开发环境下，用于热加载后重新清空所有的类
     */
    static void clearAppClasses() {
        appClasses.clear();
    }

    private static void initAppClasses() {

        initByFilePath(PathKit.getRootClassPath());

        if(excludeJars != null && !excludeJars.isEmpty()){
        	Set<String> jars = new HashSet<>();
            findJars(jars, ClassScanner.class.getClassLoader());

            for (String path : jars) {

                JarFile jarFile = null;
                try {
                    jarFile = new JarFile(path);
                    Enumeration<JarEntry> entries = jarFile.entries();
                    while (entries.hasMoreElements()) {
                        JarEntry jarEntry = entries.nextElement();
                        String entryName = jarEntry.getName();
                        if (!jarEntry.isDirectory() && entryName.endsWith(".class")) {
                            String className = entryName.replace("/", ".").substring(0, entryName.length() - 6);
                            initAppClasses(classForName(className));
                        }
                    }
                } catch (IOException e) {
                	e.printStackTrace();
                } finally {
                    if (jarFile != null)
                        try {jarFile.close();} catch (IOException e) {e.printStackTrace();}
                }
            }
        }
        
    }

    private static boolean isExcludeJar(String path) {
        for (String exclude : excludeJars) {
            if (new File(path).getName().startsWith(exclude)) {
                return true;
            }
        }

        return false;
    }

    private static void initByFilePath(String filePath) {


        List<File> classFileList = new ArrayList<>();
        scanClassFile(classFileList, filePath);
        for (File file : classFileList) {

            int start = filePath.length();
            int end = file.toString().length() - ".class".length();

            String classFile = file.toString().substring(start + 1, end);
            String className = classFile.replace(File.separator, ".");

            initAppClasses(classForName(className));
        }
    }

    private static void initAppClasses(Class<?> clazz) {
        if (clazz != null)
            appClasses.add(clazz);
    }


    private static void findJars(Set<String> set, ClassLoader classLoader) {
        try {
            if (classLoader instanceof URLClassLoader) {
                URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
                URL[] urLs = urlClassLoader.getURLs();
                for (URL url : urLs) {
                    String path = url.getPath();
                    path = URLDecoder.decode(path, JfastConstant.ENCODING_DEFAULT);
                    // path : /d:/xxx
                    if (path.startsWith("/") && path.indexOf(":") == 2) {
                        path = path.substring(1);
                    }
                    if (isExcludeJar(path)) {
                        set.add(url.getPath());
                    }
                }
            }
            ClassLoader parent = classLoader.getParent();
            if (parent != null) {
                findJars(set, parent);
            }
        } catch (Throwable ex) {
            ex.printStackTrace();
        }
    }

    private static Class<?> classForName(String className) {
        try {
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            return Class.forName(className, false, cl);
        } catch (Throwable ex) {
            //ignore
        }
        return null;
    }

    private static void scanClassFile(List<File> fileList, String path) {
        File files[] = new File(path).listFiles();
        if (null == files || files.length == 0)
            return;
        for (File file : files) {
            if (file.isDirectory()) {
                scanClassFile(fileList, file.getAbsolutePath());
            } else if (file.getName().endsWith(".class")) {
                fileList.add(file);
            }
        }
    }
}
